Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate chain interface for the coordinator package #3650

Merged
merged 11 commits into from
Jun 27, 2023

Conversation

nkuba
Copy link
Member

@nkuba nkuba commented Jun 26, 2023

In this PR we separate coordinator.Chain interface definition from the tbtc.Chain interface. This change makes the interfaces cleaner as each package requires only the functions that it is actually using.

Closes: #3632

nkuba added 9 commits June 26, 2023 14:36
We extract functions used by the ValidateDepositSweepProposal function
to a separate interface to make usage of this function easier from other
packages. With the changes here to use the ValidateDepositSweepProposal
function the chain implementation passed to the function will require
only two functions to be implemented instead of the whole tbtc.Chain
interface.
The functions used by the coordinator package should be extracted from
the pkg/tbtc to make tha packages implementation clean, as only the
functions that are used in the given package should be defined in the
chain interface for the package.

Refs: #3632
The interfaces were updated so local chain implementations doesn't have
to contain most of the functions.
We extract functions used by the ValidateRedemptionProposal function
to a separate interface to make usage of this function easier from other
packages. With the changes here to use the ValidateRedemptionProposal
function the chain implementation passed to the function will require
only two functions to be implemented instead of the whole tbtc.Chain
interface.
The changes reflect recent updates to chain interfaces.
@nkuba nkuba requested review from pdyraga and lukasz-zimnoch June 26, 2023 14:52
@nkuba nkuba self-assigned this Jun 26, 2023
@nkuba nkuba marked this pull request as ready for review June 26, 2023 14:52
@@ -359,6 +298,7 @@ type WalletCoordinatorChain interface {
// proposal events according to the provided filter or unfiltered if the
// filter is nil. Returned events are sorted by the block number in the
// ascending order, i.e. the latest event is at the end of the slice.
// TODO: This function is not used, consider removing it.
PastDepositSweepProposalSubmittedEvents(
Copy link
Member Author

@nkuba nkuba Jun 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lukasz-zimnoch Can we safely remove it? This function is not used anywhere, but there is some existing local chain implementation, which I'm not sure I can remove.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should be good to remove it. The client reacts to OnDepositSweepProposalSubmitted which pulls past events in the subscription monitoring loop. Moreover, we can check the current lock state in the WalletCoordinator contract with a view function call.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think we can remove it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetDepositRequest(
fundingTxHash bitcoin.Hash,
fundingOutputIndex uint32,
) (*tbtc.DepositChainRequest, error)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for the functions, it's clear that we want to define them separately for each package, even if it means duplication. But what about the types the functions are expecting or returning?
I don't think duplication is the right way. Alternatively to the current solution where we just import the types from the tbtc package, we could alias them, but it doesn't make much sense.

I'm curious about your opinion @pdyraga @lukasz-zimnoch.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, we should look at the packages as some bounded contexts of the same domain:

  • pkg/tbtc is the context corresponding to the tbtc wallet
  • pkg/coordinator is the context of a tbtc wallet coordinator

That said, both should define domain objects (i.e. types) and functions specific to their contexts. However, some of the domain may be shared between those two contexts and this is the case we observe here. I think we have two options:

  1. Declare the shared objects in one of the contexts
  2. Extract the shared objects to a separate place

I think option 2 is worse in our case as it will introduce unnecessary confusion. In my opinion, we should aim for option 1 as it is more "natural" for our codebase.

The only question is: "Which package should hold the shared objects?". In our case, the pkg/tbtc is kind of a core package while the pkg/coordinator is kind of an overlay specialized for the coordination part. If we agree about that, this implies that all shared objects should live in the pkg/tbtc package.

Copy link
Member

@pdyraga pdyraga Jun 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be fine with the duplication for now but I think the problem with the duplication is that those structs are separate types and the chain implementation struct would have to redeclare the same function multiple times. For example:

func GetDepositRequest(...) coordinator.DepositChainRequest // pkg/coordinator chain interface
func GetDepositRequest(...) spv.DepositChainRequest // pkg/maintainer/spv chain interface
func GetDepositRequest(...) wallet.DepositChainRequest // pkg/maintainer/wallet chain interface

The current solution is the lesser evil but I think we can do better. Instead of creating a dependency to tbtc package, we could have a separate package with the common type declarations.

pkg/chain/types/types.go

pkg/tbtc/chain.go                           // imports pkg/chain/types
pkg/coordinator/chain.go                    // imports pkg/chain/types
pkg/maintainer/wallet/chain.go              // imports pkg/chain/types
pkg/maintainer/spv/chain.go                 // imports pkg/chain/types

EDIT: Alternatively /pkg/chain/tbtc/types/types.go

Copy link
Member

@lukasz-zimnoch lukasz-zimnoch Jun 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an alternative, we could also use a similar pattern that we use in pkg/tecdsa, i.e.:

pkg
    tbtc
        wallet        // holds the current contents of pkg/tbtc
            chain.go  // interfaces and objects specific to wallet context
        coordinator   // holds the current contents of pkg/coordinator
            chain.go  // interfaces and objects specific to coordinator context
        maintainer    // holds the current contents of pkg/maintainer
        chain.go      // interfaces and objects shared by the above sub-packages

Worth noting that all sub-packages can depend on pkg/tbtc. This would correspond to option 1 from my previous comment where we extract the shared domain into a separate place.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we intentionally decided to flatten packages compared to pkg/tecdsa and treat pkg/tbtc as the client-side wallet implementation. I don't think the alternative is bad but I also like the fact the packages are currently more flat compared to pkg/tecdsa.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If pkg/tbtc is about the wallet signing logic, the auxiliary maintainer logic shouldn't import it. pkg/tbtc/types is a completely separate package from the Go perspective.

Agreed but in fact, pkg/tbtc is a little bit broader than we assume here. It also contains some logic that is useful for auxiliary packages, for example tbtc.ValidateDepositSweepProposal. That means that even if we extract types to pkg/tbtc/types, we would still have the dependency due to some shared logic. This is why I'm leaning towards leaving shared types in pkg/tbtc and do not complicate the situation with an additional package.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to elaborate more on why I'm a bit against pkg/tbtc/types:

  • It's a bit too generic and does not give a clear answer about what should live inside and what not
  • It is nested within pkg/tbtc which will make a wrong dependency direction: pkg/tbtc -> pkg/tbtc/types. In Go, we typically make sub-packages depending on the outer packages (a lot of examples in Go std lib but also ours pkg/net, pkg/tecdsa, and so on).

If we really insist on extracting, I would rather go with creating pkg/tbtccore and put there all core types and shared logic that does not belong to any specific domain context.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • It is nested within pkg/tbtc which will make a wrong dependency direction: pkg/tbtc -> pkg/tbtc/types. In Go, we typically make sub-packages depending on the outer packages (a lot of examples in Go std lib but also ours pkg/net, pkg/tecdsa, and so on).

Yes, we would have to bend this rule. It is not ideal but we wouldn't be the only ones. For example, the Block structure is defined in github.com/ethereum/go-ethereum/core/types package and it is imported by go-ethereum/core/blockchain_insert.go here. This is exactly our use case. As long as we do it only for the types package, I would be personally fine with it.

  • It's a bit too generic and does not give a clear answer about what should live inside and what not

It is less generic than tbtccore :) I see the reasoning behind the tbtccore but I am afraid this will sooner or later evolve into a common bag of stuff and create even more complicates mesh of dependencies. I think it's nothing wrong if coordinator or maintainer packages imports logic from tbtc. It is clear, they perform the same validation as the wallet as in the mentioned tbtc.ValidateDepositSweepProposal example and in my opinion, this is a really nice feature of the current code organization.

My slight preference is for a separate types package with good documentation of why it's there and what types should be there. This would give us more flexibility and avoid import cycles. But I am also fine with leaving it as-is with imports from tbtc.

Copy link
Member Author

@nkuba nkuba Jun 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, it seems that there is no perfect solution.

If we decide to declare the types in a common types package what types should go there? All the types used by chain interfaces across all the packages? We have types that are common to pkg/tbtc and pkg/coordinator and specific to the pkg/tbtc only. Which do we want to move to the common types?

Separating types from the chain interfaces declaration may be confusing, as in V2 we decided to declare the chain interfaces closer to the actual usage in packages.

Importing types defined in pkg/tbtc package into the pkg/coordinator doesn't seem perfect either. The pkg/coordinator package uses pkg/tbtc types, but also declares its' own types (e.g. NewWalletRegisteredEvent).

We cannot duplicate the types either, as the functions defined in different packages will require the same type, e.g. ValidateRedemptionProposal defined in both pkg/coordinator and pkg/tbtc must use the same RedemptionProposal type.

I lean towards the current approach where types are defined in pkg/tbtc which is considered a broader and core package and we just import them in pkg/coordinator. This isn't perfect but it doesn't require an aggressive refactor.

We could add another issue for the future to find the perfect solution for types definition.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Captured it here: #3652

@@ -359,6 +298,7 @@ type WalletCoordinatorChain interface {
// proposal events according to the provided filter or unfiltered if the
// filter is nil. Returned events are sorted by the block number in the
// ascending order, i.e. the latest event is at the end of the slice.
// TODO: This function is not used, consider removing it.
PastDepositSweepProposalSubmittedEvents(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think we can remove it.

Comment on lines 454 to 455
ValidateDepositSweepProposalChain
ValidateRedemptionProposalChain
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far, the structure of interfaces in the chain.go file reflected the contract structure we have on the chain. I think we should stick to that approach and avoid creating interfaces like ValidateDepositSweepProposalChain. If we go that way, we would soon have problems with defining the right boundaries between those interfaces. For example, ValidateDepositSweepProposalChain declares GetPendingRedemptionRequest. What if I need this function for a purpose other than proposal validation?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, ValidateDepositSweepProposalChain declares GetPendingRedemptionRequest. What if I need this function for a purpose other than proposal validation?

This is the case for #3649 currently in progress.

I do agree with this comment. I see it was done to avoid the duplication here:

tbtc.ValidateDepositSweepProposalChain
tbtc.ValidateRedemptionProposalChain

but I think having the duplication of function declarations is a better choice in this particular case. I am not a huge fan of importing types from pkg/tbtc as you already know from the other comment but importing interfaces makes it look even worse for me. 😉

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternative approach: 70bd449

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach is fine for me but I'd like to hear from @lukasz-zimnoch if it does not break anything for the automation part.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

walletLocks map[[20]byte]*walletLock
}

func newLocalChain() *localChain {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized we have a neat pattern we agreed on early in pkg/sortition where we define the local chain implementation in pkg/internal/local. This way, _test.go is only for actual tests and we can unit-test the local chain implementation as well. I know we were not very consistent in using this pattern, but this refactor feels like a good opportunity to align at least this package.

cc @lukasz-zimnoch @tomaszslabon

Copy link
Member

@lukasz-zimnoch lukasz-zimnoch Jun 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pattern can cause import cycles. Suppose we implement tbtc.Chain in pkg/tbtc/internal/local.go, then the structure declared in this internal package must refer to tbtc chain types used by tbtc.Chain. On the other hand, the unit tests living in tbtc package must create an instance of local.Chain. Here is why this pattern is used only in pkg/sortition where sorition.Chain does not declare any chain types.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... and that's why I think we should have the types declared in a separate package 😁

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know we were not very consistent in using this pattern, but this refactor feels like a good opportunity to align at least this package.

IMHO if agree on a specific pattern we should refactor and apply it to the whole keep-core project. Currently, we have multiple patterns that we use randomly in different packages. When we create a new package we're not sure which pattern to follow, and we add even more inconsistencies.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you proposing to do the all-in-one refactor in this PR?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it doesn't look like we have a consistent idea of the approach we want to follow, and it seems out-of-scope of the current PR and #3632, I propose to handle a refactor of all local chain implementations in a separate PR (along with other changes like one mutex for the local chain you proposed recently).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Captured it here: #3652

nkuba added 2 commits June 27, 2023 14:12
To reduce the confusion and make the chain interfaces consistent we
declare the functions in the given contract's Bridge and
WalletCoordinator interfaces instead of declaring separate interfaces
for each validation function.

See: #3650 (comment)
It looks like the function PastDepositSweepProposalSubmittedEvents
declared in the chain itnerface is not used anywhere so we can remove
it.
@pdyraga pdyraga merged commit 99f994a into main Jun 27, 2023
@pdyraga pdyraga deleted the coordinator-chain-reorg branch June 27, 2023 14:14
pdyraga added a commit that referenced this pull request Jun 29, 2023
Here we remove the `pkg/coordinator` by moving their contents to
`pkg/maintainer/wallet`. This package seems to be no longer needed so
this operation makes the package structure and dependency graph simpler.
This refactoring was done in several steps:

### Step 1: Make the nomenclature in `pkg/maintainer/wallet` more
precise

The tBTC v2 system defines two types of sweeps: deposit sweeps and moved
funds sweeps. In the first step, we narrowed the existing nomenclature
used within the wallet maintainer to remove ambiguity and make it clear
that the existing code refers to deposit sweeps.

### Step 2: Move the deposit sweep code from `pkg/coordinator` to
`pkg/maintainer/wallet` package

In the second step, we moved the deposit sweep code living so far in the
`pkg/coordinator`. By the way, some
simplifications were made in order to make the public API cleaner and
more readable. After this change, the `pkg/maintainer/wallet` package
exposes the following public API:
- `FindDeposits` that allows to find deposits according to the given
criteria
- `FindDepositsToSweep` that finds a batch of deposits that are most
suitable to become part of a deposit sweep
- `ProposeDepositsSweep` which submits a deposit sweep proposal to the
`WalletCoordinator` contract
- `EstimateDepositsSweepFee` that estimates the deposit sweep
transaction fee
- `DepositReference` which is a set of data allowing to identify and
refer to a deposit
- `Deposit` that holds some detailed data about a deposit
- `ParseDepositsReferences` that allows creating an array of
`DepositReference` based on the provided input string
- `ValidateDepositReferenceString` that allows to validate whether the
given string can be used to create a valid `DepositReference` instance

This step also involved moving unit tests stressing the deposit sweep
code described above.

### Step 3: Move the redemption code from `pkg/coordinator` to
`pkg/maintainer/wallet` package

In this step, we moved the redemption code from `pkg/coordinator` to the
`pkg/maintainer/wallet` package. This was similar to the previous step
but much simpler. After this step, the public API of the
`pkg/maintainer/wallet` package was extended with the following items:
- `FindPendingRedemptions` which allows to find pending redemption
requests
- `ProposeRedemption` which submits a redemption proposal to the
`WalletCoordinator` contract
- `EstimateRedemptionFee` that estimates the redemption transaction fee

This step also involved moving unit tests stressing the redemption code
described above.

### Step 4: Move the `coordinator.Chain` interface

In the fourth step, we moved the `coordinator.Chain` interface by
merging it with the `maintainer/wallet.Chain` interface.
This change clarifies the chain expectations defined by the
`pkg/maintainer/wallet` package. We also moved the remaining chain types
living so far in the `pkg/coordinator` (`coordinator.
NewWalletRegisteredEvent` and `coordinator.
NewWalletRegisteredEventFilter`) to the `pkg/tbtc` package. This package
should be the right place for them according to the recent discussion
(see
#3650 (comment)),
at least until #3652 is
done.

### Step 5: Remove `pkg/coordinator` package

In the last step, we removed the `pkg/coordinator` package and made sure
all references from the `cmd` package were correctly replaced.
@pdyraga pdyraga added this to the v2.0.0-m4 milestone Jul 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Separate chain interface for the coordinator package
3 participants